﻿using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
using System.Reflection;

namespace RootMotion
{
    public class TQ
    {
        public TQ() { }

        public TQ(Vector3 translation, Quaternion rotation)
        {
            t = translation;
            q = rotation;
        }

        public Vector3 t;
        public Quaternion q;
    }

    /*
    Written with the kind help of the one commonly known as Mecanim-Dev.
    */
    public class AvatarUtility
    {

        public static Quaternion GetPostRotation(Avatar avatar, AvatarIKGoal avatarIKGoal)
        {
            int humanId = (int)HumanIDFromAvatarIKGoal(avatarIKGoal);
            if (humanId == (int)HumanBodyBones.LastBone) throw new InvalidOperationException("Invalid human id.");

            MethodInfo methodGetPostRotation = typeof(Avatar).GetMethod("GetPostRotation", BindingFlags.Instance | BindingFlags.NonPublic);
            if (methodGetPostRotation == null) throw new InvalidOperationException("Cannot find GetPostRotation method.");

            return (Quaternion)methodGetPostRotation.Invoke(avatar, new object[] { humanId });
        }

        /// <summary>
        /// Get IK position and rotation for foot/hand bone position/rotation.
        /// </summary>
        public static TQ GetIKGoalTQ(Avatar avatar, float humanScale, AvatarIKGoal avatarIKGoal, TQ bodyPositionRotation, TQ boneTQ)
        {
            int humanId = (int)HumanIDFromAvatarIKGoal(avatarIKGoal);
            if (humanId == (int)HumanBodyBones.LastBone) throw new InvalidOperationException("Invalid human id.");

            MethodInfo methodGetAxisLength = typeof(Avatar).GetMethod("GetAxisLength", BindingFlags.Instance | BindingFlags.NonPublic);
            if (methodGetAxisLength == null) throw new InvalidOperationException("Cannot find GetAxisLength method.");

            MethodInfo methodGetPostRotation = typeof(Avatar).GetMethod("GetPostRotation", BindingFlags.Instance | BindingFlags.NonPublic);
            if (methodGetPostRotation == null) throw new InvalidOperationException("Cannot find GetPostRotation method.");

            Quaternion postRotation = (Quaternion)methodGetPostRotation.Invoke(avatar, new object[] { humanId });

            var goalTQ = new TQ(boneTQ.t, boneTQ.q * postRotation);

            if (avatarIKGoal == AvatarIKGoal.LeftFoot || avatarIKGoal == AvatarIKGoal.RightFoot)
            {
                // Here you could use animator.leftFeetBottomHeight or animator.rightFeetBottomHeight rather than GetAxisLenght
                // Both are equivalent but GetAxisLength is the generic way and work for all human bone
                float axislength = (float)methodGetAxisLength.Invoke(avatar, new object[] { humanId });
                Vector3 footBottom = new Vector3(axislength, 0, 0);
                goalTQ.t += (goalTQ.q * footBottom);
            }

            // IK goal are in avatar body local space
            Quaternion invRootQ = Quaternion.Inverse(bodyPositionRotation.q);
            goalTQ.t = invRootQ * (goalTQ.t - bodyPositionRotation.t);
            goalTQ.q = invRootQ * goalTQ.q;
            goalTQ.t /= humanScale;

            goalTQ.q = Quaternion.LookRotation(goalTQ.q * Vector3.forward, goalTQ.q * Vector3.up);

            return goalTQ;
        }

        public static TQ WorldSpaceIKGoalToBone(TQ goalTQ, Avatar avatar, AvatarIKGoal avatarIKGoal)
        {
            int humanId = (int)HumanIDFromAvatarIKGoal(avatarIKGoal);
            if (humanId == (int)HumanBodyBones.LastBone) throw new InvalidOperationException("Invalid human id.");

            MethodInfo methodGetAxisLength = typeof(Avatar).GetMethod("GetAxisLength", BindingFlags.Instance | BindingFlags.NonPublic);
            if (methodGetAxisLength == null) throw new InvalidOperationException("Cannot find GetAxisLength method.");

            MethodInfo methodGetPostRotation = typeof(Avatar).GetMethod("GetPostRotation", BindingFlags.Instance | BindingFlags.NonPublic);
            if (methodGetPostRotation == null) throw new InvalidOperationException("Cannot find GetPostRotation method.");

            Quaternion postRotation = (Quaternion)methodGetPostRotation.Invoke(avatar, new object[] { humanId });

            if (avatarIKGoal == AvatarIKGoal.LeftFoot || avatarIKGoal == AvatarIKGoal.RightFoot)
            {
                // Here you could use animator.leftFeetBottomHeight or animator.rightFeetBottomHeight rather than GetAxisLenght
                // Both are equivalent but GetAxisLength is the generic way and work for all human bone
                float axislength = (float)methodGetAxisLength.Invoke(avatar, new object[] { humanId });
                Vector3 footBottom = new Vector3(axislength, 0, 0);
                goalTQ.t -= (goalTQ.q * footBottom);
            }

            TQ boneTQ = new TQ(goalTQ.t, goalTQ.q * Quaternion.Inverse(postRotation));

            return boneTQ;
        }

        public static TQ GetWorldSpaceIKGoal(BakerHumanoidQT ikQT, BakerHumanoidQT rootQT, float time, float humanScale)
        {
            var tq = ikQT.Evaluate(time);
            var rTQ = rootQT.Evaluate(time);

            tq.q = rTQ.q * tq.q;
            tq.t = rTQ.t + rTQ.q * tq.t;
            tq.t *= humanScale;
            return tq;
        }

        public static HumanBodyBones HumanIDFromAvatarIKGoal(AvatarIKGoal avatarIKGoal)
        {
            switch (avatarIKGoal)
            {
                case AvatarIKGoal.LeftFoot: return HumanBodyBones.LeftFoot;
                case AvatarIKGoal.RightFoot: return HumanBodyBones.RightFoot;
                case AvatarIKGoal.LeftHand: return HumanBodyBones.LeftHand;
                case AvatarIKGoal.RightHand: return HumanBodyBones.RightHand;
                default: return HumanBodyBones.LastBone;
            }
        }
    }
}